/*
 * Copyright (c) Baidu Inc. All rights reserved.
 * 
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.baidu.bjf.remoting.protobuf.utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * A compact template engine for HTML files.
 *
 * <p>
 * Template syntax:<br>
 * 
 * <pre>
*    Variables:
*       ${VariableName}
*
*    Blocks:
*       &lt;!-- $beginBlock blockName --&gt;
*         ... block contents ...
*       &lt;!-- $endBlock blockName --&gt;
*
*    Conditional blocks:
*       &lt;!-- $if flag1 flag2 --&gt;
*         ... included if flag1 or flag2 is set ...
*       &lt;!-- $elseIf !flag3 flag4 --&gt;
*         ... included if flag3 is not set or flag4 is set ...
*       &lt;!-- $else --&gt;
*         ... included if none of the above conditions is met ...
*       &lt;!-- $endIf --&gt;
*
*    Short form of conditional blocks:
*    (only recognized if {@link TemplateSpecification#shortFormEnabled TemplateSpecification.shortFormEnabled} is <code>true</code>)
*       &lt;$? flag1 flag2 &gt;
*         ... included if flag1 or flag2 is set ...
*       &lt;$: !flag3 flag4 &gt;
*         ... included if flag3 is not set or flag4 is set ...
*       &lt;$:&gt;
*         ... included if none of the above conditions is met ...
*       &lt;$/?&gt;
*    Example:
*       &lt;$?de&gt; Hallo Welt!
*       &lt;$:fr&gt; Bonjour tout le monde!
*       &lt;$:  &gt; Hello world!
*       &lt;$/?&gt;
*
*    Include a subtemplate:
*       &lt;!-- $include relativeFileName --&gt;
 * </pre>
 *
 * <p>
 * General remarks:
 * </p>
 * <ul>
 * <li>Variable names, block names, condition flags and commands (e.g. "$beginBlock") are case-insensitive.</li>
 * <li>The same variable may be used multiple times within a template.</li>
 * <li>Multiple blocks with the same name may occur within a template.</li>
 * <li>Blocks can be nested.</li>
 * <li>Conditional blocks ($if) and includes ($include) are resolved when the template is parsed. Parsing is done within
 * the MiniTemplator constructor. Condition flags can be passed to the constructor using {@link TemplateSpecification}.
 * <li>Normal blocks ($beginBlock) must be added (and can be repeated) by the application program using
 * <code>addBlock()</code>.
 * <li>The {@link MiniTemplatorCache} class may be used to cache MiniTemplator objects with parsed templates.</li>
 * </ul>
 *
 * <p>
 * Project home page: <a href="http://www.source-code.biz/MiniTemplator">www.source-code.biz/MiniTemplator</a><br>
 * Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland
 */
public class MiniTemplator {

    // --- exceptions -----------------------------------------------------

    /**
     * Thrown when a syntax error is encountered within the template.
     */
    public static class TemplateSyntaxException extends RuntimeException {
        private static final long serialVersionUID = 1;

        public TemplateSyntaxException(String msg) {
            super("Syntax error in template: " + msg);
        }
    }

    /**
     * Thrown when {@link MiniTemplator#setVariable(String, String, boolean) Minitemplator.setVariable} is called with a
     * <code>variableName</code> that is not defined within the template and the <code>isOptional</code> parameter is
     * <code>false</code>.
     */
    public static class VariableNotDefinedException extends RuntimeException {
        private static final long serialVersionUID = 1;

        public VariableNotDefinedException(String variableName) {
            super("Variable \"" + variableName + "\" not defined in template.");
        }
    }

    /**
     * Thrown when {@link MiniTemplator#addBlock Minitemplator.addBlock} is called with a <code>blockName</code> that is
     * not defined within the template.
     */
    public static class BlockNotDefinedException extends RuntimeException {
        private static final long serialVersionUID = 1;

        public BlockNotDefinedException(String blockName) {
            super("Block \"" + blockName + "\" not defined in template.");
        }
    }

    // --- public nested classes ------------------------------------------

    /**
     * Specifies the parameters for constructing a {@link MiniTemplator} object.
     */
    public static class TemplateSpecification { // template specification

        /**
         * The file name of the template file.
         */
        public String templateFileName;

        /** The template file stream. */
        public InputStream templateFileStream;

        /**
         * The path of the base directory for reading subtemplate files. This path is used to convert the relative paths
         * of subtemplate files (specified with the $include commands) into absolute paths. If this field is null, the
         * parent directory of the main template file (specified by <code>templateFileName</code>) is used.
         */
        public String subtemplateBasePath;

        /**
         * The character set to be used for reading and writing files. This charset is used for reading the template and
         * subtemplate files and for writing output with {@link #generateOutput(String outputFileName)}. If this field
         * is null, the default charset of the Java VM is used.
         */
        public Charset charset;

        /**
         * The contents of the template file. This field may be used instead of <code>templateFileName</code> to pass
         * the template text in memory. If this field is not null, <code>templateFileName</code> will be ignored.
         */
        public String templateText;

        /**
         * Flags for the conditional commands ($if, $elseIf). A set of flag names, that can be used with the $if and
         * $elseIf commands. The flag names are case-insensitive.
         */
        public Set<String> conditionFlags;

        /**
         * Enables the short form syntax for conditional blocks.
         */
        public boolean shortFormEnabled;
    }

    // --- private nested classes -----------------------------------------

    private static class BlockDynTabRec { // block dynamic data table record structure
        int instances; // number of instances of this block
        int firstBlockInstNo; // block instance no of first instance of this block or -1
        int lastBlockInstNo; // block instance no of last instance of this block or -1
        int currBlockInstNo;
    } // current block instance no, used during generation of output file

    private static class BlockInstTabRec { // block instance table record structure
        int blockNo; // block number
        int instanceLevel; // instance level of this block
        // InstanceLevel is an instance counter per block.
        // (In contrast to blockInstNo, which is an instance counter over the instances of all blocks)
        int parentInstLevel; // instance level of parent block
        int nextBlockInstNo; // pointer to next instance of this block or -1
        // Forward chain for instances of same block.
        String[] blockVarTab;
    } // block instance variables

    // --- private variables ----------------------------------------------

    private MiniTemplatorParser mtp; // contains the parsed template
    private Charset charset; // charset used for reading and writing files
    private String subtemplateBasePath; // base path for relative file names of subtemplates, may be null

    private String[] varValuesTab; // variable values table, entries may be null

    private BlockDynTabRec[] blockDynTab; // dynamic block-specific values
    private BlockInstTabRec[] blockInstTab; // block instances table
    // This table contains an entry for each block instance that has been added.
    // Indexed by BlockInstNo.
    private int blockInstTabCnt; // no of entries used in BlockInstTab

    // --- constructors ---------------------------------------------------

    /**
     * Constructs a MiniTemplator object.
     * <p>
     * During construction, the template and subtemplate files are read and parsed.
     * <p>
     * Note: The {@link MiniTemplatorCache} class may be used to cache MiniTemplator objects.
     * 
     * @param templateSpec the template specification.
     * @throws TemplateSyntaxException when a syntax error is detected within the template.
     * @throws IOException when an i/o error occurs while reading the template.
     */
    public MiniTemplator(TemplateSpecification templateSpec) throws IOException, TemplateSyntaxException {
        init(templateSpec);
    }

    /**
     * Constructs a MiniTemplator object by specifying only the file name.
     * <p>
     * This is a convenience constructor that may be used when only the file name has to be specified.
     * 
     * @param templateFileName the file name of the template file.
     * @throws TemplateSyntaxException when a syntax error is detected within the template.
     * @throws IOException when an i/o error occurs while reading the template.
     * @see #MiniTemplator(TemplateSpecification)
     */
    public MiniTemplator(String templateFileName, InputStream templateFileStream)
            throws IOException, TemplateSyntaxException {
        TemplateSpecification templateSpec = new TemplateSpecification();
        templateSpec.templateFileName = templateFileName;
        templateSpec.templateFileStream = templateFileStream;
        init(templateSpec);
    }

    private void init(TemplateSpecification templateSpec) throws IOException, TemplateSyntaxException {
        charset = templateSpec.charset;
        if (charset == null) {
            charset = Charset.defaultCharset();
        }
        subtemplateBasePath = templateSpec.subtemplateBasePath;
        if (subtemplateBasePath == null && templateSpec.templateFileName != null) {
            subtemplateBasePath = new File(templateSpec.templateFileName).getParent();
        }
        String templateText = templateSpec.templateText;
        if (templateText == null && templateSpec.templateFileName != null) {
            templateText = readFileIntoString(templateSpec.templateFileName, templateSpec.templateFileStream);
        }
        if (templateText == null) {
            throw new IllegalArgumentException("No templateFileName or templateText specified.");
        }
        mtp = new MiniTemplatorParser(templateText, templateSpec.conditionFlags, templateSpec.shortFormEnabled, this);
        reset();
    }

    /**
     * Dummy constructor, used internally in newInstance().
     */
    protected MiniTemplator() {
    }

    /**
     * Allocates a new uninitialized MiniTemplator object. This method is intended to be overridden in a derived class.
     * It is called from cloneReset() to create a new MiniTemplator object.
     */
    protected MiniTemplator newInstance() {
        return new MiniTemplator();
    }

    // --- loadSubtemplate ------------------------------------------------

    /**
     * Loads the template string of a subtemplate (used for the $Include command). This method can be overridden in a
     * subclass, to load subtemplates from somewhere else, e.g. from a database.
     * <p>
     * This implementation of the method interprets <code>subtemplateName</code> as a relative file path name and reads
     * the template string from that file. {@link MiniTemplator.TemplateSpecification#subtemplateBasePath} is used to
     * convert the relative path of the subtemplate into an absolute path.
     * 
     * @param subtemplateName the name of the subtemplate. Normally a relative file path. This is the argument string
     *            that was specified with the "$Include" command. If the string has quotes, the quotes are removed
     *            before this method is called.
     * @return the template text string of the subtemplate.
     */
    protected String loadSubtemplate(String subtemplateName) throws IOException {
        return readFileIntoString(subtemplateName, null);
    }

    // --- build up (template variables and blocks) ------------------------

    /**
     * Resets the MiniTemplator object to the initial state. All variable values are cleared and all added block
     * instances are deleted. This method can be used to produce another HTML page with the same template. It is faster
     * than creating another MiniTemplator object, because the template does not have to be read and parsed again.
     */
    public void reset() {
        if (varValuesTab == null) {
            varValuesTab = new String[mtp.varTabCnt];
        } else {
            for (int varNo = 0; varNo < mtp.varTabCnt; varNo++) {
                varValuesTab[varNo] = null;
            }
        }
        if (blockDynTab == null) {
            blockDynTab = new BlockDynTabRec[mtp.blockTabCnt];
        }
        for (int blockNo = 0; blockNo < mtp.blockTabCnt; blockNo++) {
            BlockDynTabRec bdtr = blockDynTab[blockNo];
            if (bdtr == null) {
                bdtr = new BlockDynTabRec();
                blockDynTab[blockNo] = bdtr;
            }
            bdtr.instances = 0;
            bdtr.firstBlockInstNo = -1;
            bdtr.lastBlockInstNo = -1;
        }
        blockInstTabCnt = 0;
    }

    /**
     * Clones this MiniTemplator object and resets the clone. This method is used to copy a MiniTemplator object. It is
     * fast, because the template does not have to be parsed again, and the internal data structures that contain the
     * parsed template information are shared among the clones.
     * <p>
     * This method is used by the {@link MiniTemplatorCache} class to clone the cached MiniTemplator objects.
     */
    public MiniTemplator cloneReset() {
        MiniTemplator m = newInstance();
        m.mtp = mtp; // the MiniTemplatorParser object is shared among the clones
        m.charset = charset;
        // (subtemplateBasePath does not have to be copied, because the subtemplates have already been read)
        m.reset();
        return m;
    }

    /**
     * Sets a template variable.
     * <p>
     * For variables that are used in blocks, the variable value must be set before <code>addBlock()</code> is called.
     * 
     * @param variableName the name of the variable to be set. Case-insensitive.
     * @param variableValue the new value of the variable. May be <code>null</code>.
     * @param isOptional specifies whether an exception should be thrown when the variable does not exist in the
     *            template. If <code>isOptional</code> is <code>false</code> and the variable does not exist, an
     *            exception is thrown.
     * @throws VariableNotDefinedException when no variable with the specified name exists in the template and
     *             <code>isOptional</code> is <code>false</code>.
     */
    public void setVariable(String variableName, String variableValue, boolean isOptional)
            throws VariableNotDefinedException {
        int varNo = mtp.lookupVariableName(variableName);
        if (varNo == -1) {
            if (isOptional) {
                return;
            }
            throw new VariableNotDefinedException(variableName);
        }
        varValuesTab[varNo] = variableValue;
    }

    /**
     * Sets a template variable.
     * <p>
     * Convenience method for: <code>setVariable (variableName, variableValue, false)</code>
     * 
     * @param variableName the name of the variable to be set. Case-insensitive.
     * @param variableValue the new value of the variable. May be <code>null</code>.
     * @throws VariableNotDefinedException when no variable with the specified name exists in the template.
     * @see #setVariable(String, String, boolean)
     */
    public void setVariable(String variableName, String variableValue) throws VariableNotDefinedException {
        setVariable(variableName, variableValue, false);
    }

    /**
     * Sets a template variable to an integer value.
     * <p>
     * Convenience method for: <code>setVariable (variableName, Integer.toString(variableValue))</code>
     * 
     * @param variableName the name of the variable to be set. Case-insensitive.
     * @param variableValue the new value of the variable.
     * @throws VariableNotDefinedException when no variable with the specified name exists in the template.
     */
    public void setVariable(String variableName, int variableValue) throws VariableNotDefinedException {
        setVariable(variableName, Integer.toString(variableValue));
    }

    /**
     * Sets an optional template variable.
     * <p>
     * Convenience method for: <code>setVariable (variableName, variableValue, true)</code>
     * 
     * @param variableName the name of the variable to be set. Case-insensitive.
     * @param variableValue the new value of the variable. May be <code>null</code>.
     * @see #setVariable(String, String, boolean)
     */
    public void setVariableOpt(String variableName, String variableValue) {
        setVariable(variableName, variableValue, true);
    }

    /**
     * Sets an optional template variable to an integer value.
     * <p>
     * Convenience method for: <code>setVariableOpt (variableName, Integer.toString(variableValue))</code>
     * 
     * @param variableName the name of the variable to be set. Case-insensitive.
     * @param variableValue the new value of the variable.
     */
    public void setVariableOpt(String variableName, int variableValue) {
        // We want to avoid the integer to string conversion if the template variable does not exist.
        int varNo = mtp.lookupVariableName(variableName);
        if (varNo == -1) {
            return;
        }
        varValuesTab[varNo] = Integer.toString(variableValue);
    }

    /**
     * Sets a template variable to an escaped value.
     * <p>
     * Convenience method for:
     * <code>setVariable (variableName, MiniTemplator.escapeHtml(variableValue), isOptional)</code>
     * 
     * @param variableName the name of the variable to be set.
     * @param variableValue the new value of the variable. May be <code>null</code>. Special HTML/XML characters are
     *            escaped.
     * @param isOptional specifies whether an exception should be thrown when the variable does not exist in the
     *            template. If <code>isOptional</code> is <code>false</code> and the variable does not exist, an
     *            exception is thrown.
     * @throws VariableNotDefinedException when no variable with the specified name exists in the template and
     *             <code>isOptional</code> is <code>false</code>.
     * @see #setVariable(String, String, boolean)
     * @see #escapeHtml(String)
     */
    public void setVariableEsc(String variableName, String variableValue, boolean isOptional)
            throws VariableNotDefinedException {
        setVariable(variableName, escapeHtml(variableValue), isOptional);
    }

    /**
     * Sets a template variable to an escaped value.
     * <p>
     * Convenience method for: <code>setVariable (variableName, MiniTemplator.escapeHtml(variableValue), false)</code>
     * 
     * @param variableName the name of the variable to be set. Case-insensitive.
     * @param variableValue the new value of the variable. May be <code>null</code>. Special HTML/XML characters are
     *            escaped.
     * @throws VariableNotDefinedException when no variable with the specified name exists in the template.
     * @see #setVariable(String, String, boolean)
     * @see #escapeHtml(String)
     */
    public void setVariableEsc(String variableName, String variableValue) throws VariableNotDefinedException {
        setVariable(variableName, escapeHtml(variableValue), false);
    }

    /**
     * Sets an optional template variable to an escaped value.
     * <p>
     * Convenience method for: <code>setVariable (variableName, MiniTemplator.escapeHtml(variableValue), true)</code>
     * 
     * @param variableName the name of the variable to be set. Case-insensitive.
     * @param variableValue the new value of the variable. May be <code>null</code>. Special HTML/XML characters are
     *            escaped.
     * @see #setVariable(String, String, boolean)
     * @see #escapeHtml(String)
     */
    public void setVariableOptEsc(String variableName, String variableValue) {
        setVariable(variableName, escapeHtml(variableValue), true);
    }

    /**
     * Checks whether a variable with the specified name exists within the template.
     * 
     * @param variableName the name of the variable. Case-insensitive.
     * @return <code>true</code> if the variable exists.<br>
     *         <code>false</code> if no variable with the specified name exists in the template.
     */
    public boolean variableExists(String variableName) {
        return mtp.lookupVariableName(variableName) != -1;
    }

    /**
     * Returns a map with the names and current values of the template variables.
     */
    public Map<String, String> getVariables() {
        HashMap<String, String> map = new HashMap<String, String>(mtp.varTabCnt);
        for (int varNo = 0; varNo < mtp.varTabCnt; varNo++)
            map.put(mtp.varTab[varNo], varValuesTab[varNo]);
        return map;
    }

    /**
     * Adds an instance of a template block.
     * <p>
     * If the block contains variables, these variables must be set before the block is added. If the block contains
     * subblocks (nested blocks), the subblocks must be added before this block is added. If multiple blocks exist with
     * the specified name, an instance is added for each block occurrence.
     * 
     * @param blockName the name of the block to be added. Case-insensitive.
     * @param isOptional specifies whether an exception should be thrown when the block does not exist in the template.
     *            If <code>isOptional</code> is <code>false</code> and the block does not exist, an exception is thrown.
     * @throws BlockNotDefinedException when no block with the specified name exists in the template and
     *             <code>isOptional</code> is <code>false</code>.
     */
    public void addBlock(String blockName, boolean isOptional) throws BlockNotDefinedException {
        int blockNo = mtp.lookupBlockName(blockName);
        if (blockNo == -1) {
            if (isOptional) {
                return;
            }
            throw new BlockNotDefinedException(blockName);
        }
        while (blockNo != -1) {
            addBlockByNo(blockNo);
            blockNo = mtp.blockTab[blockNo].nextWithSameName;
        }
    }

    /**
     * Adds an instance of a template block.
     * <p>
     * Convenience method for: <code>addBlock (blockName, false)</code>
     * 
     * @param blockName the name of the block to be added. Case-insensitive.
     * @throws BlockNotDefinedException when no block with the specified name exists in the template.
     * @see #addBlock(String, boolean)
     */
    public void addBlock(String blockName) throws BlockNotDefinedException {
        addBlock(blockName, false);
    }

    /**
     * Adds an instance of an optional template block.
     * <p>
     * Convenience method for: <code>addBlock (blockName, true)</code>
     * 
     * @param blockName the name of the block to be added. Case-insensitive.
     * @see #addBlock(String, boolean)
     */
    public void addBlockOpt(String blockName) {
        addBlock(blockName, true);
    }

    private void addBlockByNo(int blockNo) {
        MiniTemplatorParser.BlockTabRec btr = mtp.blockTab[blockNo];
        BlockDynTabRec bdtr = blockDynTab[blockNo];
        int blockInstNo = registerBlockInstance();
        BlockInstTabRec bitr = blockInstTab[blockInstNo];
        if (bdtr.firstBlockInstNo == -1) {
            bdtr.firstBlockInstNo = blockInstNo;
        }
        if (bdtr.lastBlockInstNo != -1) {
            blockInstTab[bdtr.lastBlockInstNo].nextBlockInstNo = blockInstNo;
        } // set forward pointer of chain
        bdtr.lastBlockInstNo = blockInstNo;
        bitr.blockNo = blockNo;
        bitr.instanceLevel = bdtr.instances++;
        if (btr.parentBlockNo == -1) {
            bitr.parentInstLevel = -1;
        } else {
            bitr.parentInstLevel = blockDynTab[btr.parentBlockNo].instances;
        }
        bitr.nextBlockInstNo = -1;
        if (btr.blockVarCnt > 0) {
            bitr.blockVarTab = new String[btr.blockVarCnt];
        }
        for (int blockVarNo = 0; blockVarNo < btr.blockVarCnt; blockVarNo++) { // copy instance variables for this block
            int varNo = btr.blockVarNoToVarNoMap[blockVarNo];
            bitr.blockVarTab[blockVarNo] = varValuesTab[varNo];
        }
    }

    // Returns the block instance number.
    private int registerBlockInstance() {
        int blockInstNo = blockInstTabCnt++;
        if (blockInstTab == null) {
            blockInstTab = new BlockInstTabRec[64];
        }
        if (blockInstTabCnt > blockInstTab.length) {
            blockInstTab = (BlockInstTabRec[]) MiniTemplatorParser.resizeArray(blockInstTab, 2 * blockInstTabCnt);
        }
        blockInstTab[blockInstNo] = new BlockInstTabRec();
        return blockInstNo;
    }

    /**
     * Checks whether a block with the specified name exists within the template.
     * 
     * @param blockName the name of the block.
     * @return <code>true</code> if the block exists.<br>
     *         <code>false</code> if no block with the specified name exists in the template.
     */
    public boolean blockExists(String blockName) {
        return mtp.lookupBlockName(blockName) != -1;
    }

    // --- output generation ----------------------------------------------

    /**
     * Generates the HTML page and writes it into a file.
     * 
     * @param outputFileName name of the file to which the generated HTML page will be written.
     * @throws IOException when an i/o error occurs while writing to the file.
     */
    public void generateOutput(String outputFileName) throws IOException {
        FileOutputStream stream = null;
        OutputStreamWriter writer = null;
        try {
            stream = new FileOutputStream(outputFileName);
            writer = new OutputStreamWriter(stream, charset);
            generateOutput(writer);
        } finally {
            if (writer != null) {
                writer.close();
            }
            if (stream != null) {
                stream.close();
            }
        }
    }

    /**
     * Generates the HTML page and writes it to a character stream.
     * 
     * @param outputWriter a character stream (<code>writer</code>) to which the HTML page will be written.
     * @throws IOException when an i/o error occurs while writing to the stream.
     */
    public void generateOutput(Writer outputWriter) throws IOException {
        String s = generateOutput();
        outputWriter.write(s);
    }

    /**
     * Generates the HTML page and returns it as a string.
     * 
     * @return A string that contains the generated HTML page.
     */
    public String generateOutput() {
        if (blockDynTab[0].instances == 0) {
            addBlockByNo(0);
        } // add main block
        for (int blockNo = 0; blockNo < mtp.blockTabCnt; blockNo++) {
            BlockDynTabRec bdtr = blockDynTab[blockNo];
            bdtr.currBlockInstNo = bdtr.firstBlockInstNo;
        }
        StringBuilder out = new StringBuilder();
        writeBlockInstances(out, 0, -1);
        return out.toString();
    }

    // Writes all instances of a block that are contained within a specific
    // parent block instance.
    // Called recursively.
    private void writeBlockInstances(StringBuilder out, int blockNo, int parentInstLevel) {
        BlockDynTabRec bdtr = blockDynTab[blockNo];
        while (true) {
            int blockInstNo = bdtr.currBlockInstNo;
            if (blockInstNo == -1) {
                break;
            }
            BlockInstTabRec bitr = blockInstTab[blockInstNo];
            if (bitr.parentInstLevel < parentInstLevel) {
                throw new AssertionError();
            }
            if (bitr.parentInstLevel > parentInstLevel) {
                break;
            }
            writeBlockInstance(out, blockInstNo);
            bdtr.currBlockInstNo = bitr.nextBlockInstNo;
        }
    }

    private void writeBlockInstance(StringBuilder out, int blockInstNo) {
        BlockInstTabRec bitr = blockInstTab[blockInstNo];
        int blockNo = bitr.blockNo;
        MiniTemplatorParser.BlockTabRec btr = mtp.blockTab[blockNo];
        int tPos = btr.tPosContentsBegin;
        int subBlockNo = blockNo + 1;
        int varRefNo = btr.firstVarRefNo;
        while (true) {
            int tPos2 = btr.tPosContentsEnd;
            int kind = 0; // assume end-of-block
            if (varRefNo != -1 && varRefNo < mtp.varRefTabCnt) { // check for variable reference
                MiniTemplatorParser.VarRefTabRec vrtr = mtp.varRefTab[varRefNo];
                if (vrtr.tPosBegin < tPos) {
                    varRefNo++;
                    continue;
                }
                if (vrtr.tPosBegin < tPos2) {
                    tPos2 = vrtr.tPosBegin;
                    kind = 1;
                }
            }
            if (subBlockNo < mtp.blockTabCnt) { // check for subblock
                MiniTemplatorParser.BlockTabRec subBtr = mtp.blockTab[subBlockNo];
                if (subBtr.tPosBegin < tPos) {
                    subBlockNo++;
                    continue;
                }
                if (subBtr.tPosBegin < tPos2) {
                    tPos2 = subBtr.tPosBegin;
                    kind = 2;
                }
            }
            if (tPos2 > tPos) {
                out.append(mtp.templateText.substring(tPos, tPos2));
            }
            switch (kind) {
                case 0: {// end of block
                    return;
                }
                case 1: { // variable
                    MiniTemplatorParser.VarRefTabRec vrtr = mtp.varRefTab[varRefNo];
                    if (vrtr.blockNo != blockNo) {
                        throw new AssertionError();
                    }
                    String variableValue = bitr.blockVarTab[vrtr.blockVarNo];
                    if (variableValue != null) {
                        out.append(variableValue);
                    }
                    tPos = vrtr.tPosEnd;
                    varRefNo++;
                    break;
                }
                case 2: { // sub block
                    MiniTemplatorParser.BlockTabRec subBtr = mtp.blockTab[subBlockNo];
                    if (subBtr.parentBlockNo != blockNo) {
                        throw new AssertionError();
                    }
                    writeBlockInstances(out, subBlockNo, bitr.instanceLevel); // recursive call
                    tPos = subBtr.tPosEnd;
                    subBlockNo++;
                    break;
                }
            }
        }
    }

    // --- general utility routines ---------------------------------------

    // Reads the contents of a file into a string variable.
    private String readFileIntoString(String fileName,  InputStream is) throws IOException {
        FileInputStream stream = null;
        InputStreamReader reader = null;
        try {
            if (is != null) {
                reader = new InputStreamReader(is, charset);
            } else {
                stream = new FileInputStream(fileName);
                reader = new InputStreamReader(stream, charset);
            }
            
            return readStreamIntoString(reader);
        } finally {
            if (reader != null) {
                reader.close();
            }
            if (stream != null) {
                stream.close();
            }
        }
    }

    // Reads the contents of a stream into a string variable.
    private static String readStreamIntoString(Reader reader) throws IOException {
        StringBuilder s = new StringBuilder();
        char a[] = new char[0x10000];
        while (true) {
            int l = reader.read(a);
            if (l == -1) {
                break;
            }
            if (l <= 0) {
                throw new IOException();
            }
            s.append(a, 0, l);
        }
        return s.toString();
    }

    /**
     * Escapes special HTML characters. Replaces the characters &lt;, &gt;, &amp;, ' and " by their corresponding
     * HTML/XML character entity codes.
     * 
     * @param s the input string.
     * @return the escaped output string.
     */
    public static String escapeHtml(String s) {
        // (The code of this method is a bit redundant in order to optimize speed)
        if (s == null) {
            return null;
        }
        int sLength = s.length();
        boolean found = false;
        int p;
        loop1: for (p = 0; p < sLength; p++) {
            switch (s.charAt(p)) {
                case '<':
                case '>':
                case '&':
                case '\'':
                case '"':
                    found = true;
                    break loop1;
            }
        }
        if (!found) {
            return s;
        }
        StringBuilder sb = new StringBuilder(sLength + 16);
        sb.append(s.substring(0, p));
        for (; p < sLength; p++) {
            char c = s.charAt(p);
            switch (c) {
                case '<':
                    sb.append("&lt;");
                    break;
                case '>':
                    sb.append("&gt;");
                    break;
                case '&':
                    sb.append("&amp;");
                    break;
                case '\'':
                    sb.append("&#39;");
                    break;
                case '"':
                    sb.append("&#34;");
                    break;
                default:
                    sb.append(c);
            }
        }
        return sb.toString();
    }

} // End class MiniTemplator

// ====================================================================================================================

// MiniTemplatorParser is an immutable object that contains the parsed template text.
class MiniTemplatorParser {

    // --- constants ------------------------------------------------------

    private static final int maxNestingLevel = 20; // maximum number of block nestings
    private static final int maxCondLevels = 20; // maximum number of nested conditional commands ($if)
    private static final int maxInclTemplateSize = 1000000; // maximum length of template string when including
                                                            // subtemplates
    private static final String cmdStartStr = "<!--"; // command start string
    private static final String cmdEndStr = "-->"; // command end string
    private static final String cmdStartStrShort = "<$"; // short form command start string
    private static final String cmdEndStrShort = ">"; // short form command end string

    // --- nested classes -------------------------------------------------

    public static class VarRefTabRec { // variable reference table record structure
        int varNo; // variable no
        int tPosBegin; // template position of begin of variable reference
        int tPosEnd; // template position of end of variable reference
        int blockNo; // block no of the (innermost) block that contains this variable reference
        int blockVarNo;
    } // block variable no. Index into BlockInstTab.BlockVarTab

    public static class BlockTabRec { // block table record structure
        String blockName; // block name
        int nextWithSameName; // block no of next block with same name or -1 (blocks are backward linked related to
                              // their position within the template)
        int tPosBegin; // template position of begin of block
        int tPosContentsBegin; // template pos of begin of block contents
        int tPosContentsEnd; // template pos of end of block contents
        int tPosEnd; // template position of end of block
        int nestingLevel; // block nesting level
        int parentBlockNo; // block no of parent block
        boolean definitionIsOpen; // true while $BeginBlock processed but no $EndBlock
        int blockVarCnt; // number of variables in block
        int[] blockVarNoToVarNoMap; // maps block variable numbers to variable numbers
        int firstVarRefNo; // variable reference no of first variable of this block or -1
        boolean dummy;
    } // true if this is a dummy block that will never be included in the output

    // --- variables ------------------------------------------------------

    public String templateText; // contents of the template file
    private HashSet<String> conditionFlags; // set of the condition flags, converted to uppercase
    private boolean shortFormEnabled; // true to enable the short form of commands ("<$...>")

    public String[] varTab; // variables table, contains variable names, array index is variable no
    public int varTabCnt; // no of entries used in VarTab
    private HashMap<String, Integer> varNameToNoMap; // maps variable names to variable numbers
    public VarRefTabRec[] varRefTab; // variable references table
    // Contains an entry for each variable reference in the template. Ordered by templatePos.
    public int varRefTabCnt; // no of entries used in VarRefTab

    public BlockTabRec[] blockTab; // Blocks table, array index is block no
    // Contains an entry for each block in the template. Ordered by tPosBegin.
    public int blockTabCnt; // no of entries used in BlockTab
    private HashMap<String, Integer> blockNameToNoMap; // maps block names to block numbers

    // The following variables are only used temporarilly during parsing of the template.
    private int currentNestingLevel; // current block nesting level during parsing
    private int[] openBlocksTab; // indexed by the block nesting level
    // During parsing, this table contains the block numbers of the open parent blocks (nested outer blocks).
    private int condLevel; // current nesting level of conditional commands ($if), -1 = main level
    private boolean[] condEnabled; // enabled/disables state for the conditions of each level
    private boolean[] condPassed; // true if an enabled condition clause has already been processed (separate for each
                                  // level)
    private MiniTemplator miniTemplator; // the MiniTemplator who created this parser object
    // The reference to the MiniTemplator object is only used to call MiniTemplator.loadSubtemplate().
    private boolean resumeCmdParsingFromStart; // true = resume command parsing from the start position of the last
                                               // command

    // --- constructor ----------------------------------------------------

    // (The MiniTemplator object is only passed to the parser, because the
    // parser needs to call MiniTemplator.loadSubtemplate() to load subtemplates.)
    public MiniTemplatorParser(String templateText, Set<String> conditionFlags, boolean shortFormEnabled,
            MiniTemplator miniTemplator) throws MiniTemplator.TemplateSyntaxException {
        this.templateText = templateText;
        this.conditionFlags = createConditionFlagsSet(conditionFlags);
        this.shortFormEnabled = shortFormEnabled;
        this.miniTemplator = miniTemplator;
        parseTemplate();
        this.miniTemplator = null;
    }

    private HashSet<String> createConditionFlagsSet(Set<String> flags) {
        if (flags == null || flags.isEmpty()) {
            return null;
        }
        HashSet<String> flags2 = new HashSet<String>(flags.size());
        for (String flag : flags) {
            flags2.add(flag.toUpperCase());
        }
        return flags2;
    }

    // --- template parsing -----------------------------------------------

    private void parseTemplate() throws MiniTemplator.TemplateSyntaxException {
        initParsing();
        beginMainBlock();
        parseTemplateCommands();
        endMainBlock();
        checkBlockDefinitionsComplete();
        if (condLevel != -1) {
            throw new MiniTemplator.TemplateSyntaxException("$if without matching $endIf.");
        }
        parseTemplateVariables();
        associateVariablesWithBlocks();
        terminateParsing();
    }

    private void initParsing() {
        varTab = new String[64];
        varTabCnt = 0;
        varNameToNoMap = new HashMap<String, Integer>();
        varRefTab = new VarRefTabRec[64];
        varRefTabCnt = 0;
        blockTab = new BlockTabRec[16];
        blockTabCnt = 0;
        currentNestingLevel = 0;
        blockNameToNoMap = new HashMap<String, Integer>();
        openBlocksTab = new int[maxNestingLevel + 1];
        condLevel = -1;
        condEnabled = new boolean[maxCondLevels];
        condPassed = new boolean[maxCondLevels];
    }

    private void terminateParsing() {
        openBlocksTab = null;
    }

    // Registers the main block.
    // The main block is an implicitly defined block that covers the whole template.
    private void beginMainBlock() {
        int blockNo = registerBlock(null); // =0
        BlockTabRec btr = blockTab[blockNo];
        btr.tPosBegin = 0;
        btr.tPosContentsBegin = 0;
        openBlocksTab[currentNestingLevel] = blockNo;
        currentNestingLevel++;
    }

    // Completes the main block registration.
    private void endMainBlock() {
        BlockTabRec btr = blockTab[0];
        btr.tPosContentsEnd = templateText.length();
        btr.tPosEnd = templateText.length();
        btr.definitionIsOpen = false;
        currentNestingLevel--;
    }

    // --- Template commands --------------------------------------------------------

    // Parses commands within the template in the format "<!-- $command parameters -->".
    // If shortFormEnabled is true, the short form commands in the format "<$...>" are also recognized.
    private void parseTemplateCommands() throws MiniTemplator.TemplateSyntaxException {
        int p = 0; // p is the current position within templateText
        while (true) {
            int p0 = templateText.indexOf(cmdStartStr, p); // p0 is the start of the current command
            boolean shortForm = false;
            if (shortFormEnabled && p0 != p) {
                if (p0 == -1) {
                    p0 = templateText.indexOf(cmdStartStrShort, p);
                    shortForm = true;
                } else {
                    int p2 = templateText.substring(p, p0).indexOf(cmdStartStrShort);
                    if (p2 != -1) {
                        p0 = p + p2;
                        shortForm = true;
                    }
                }
            }
            if (p0 == -1) { // no more commands
                break;
            }
            conditionalExclude(p, p0); // process text up to the start of the current command
            if (shortForm) { // short form command
                p = templateText.indexOf(cmdEndStrShort, p0 + cmdStartStrShort.length());
                if (p == -1) { // if no terminating ">" is found, we process it as normal text
                    p = p0 + cmdStartStrShort.length();
                    conditionalExclude(p0, p);
                    continue;
                }
                p += cmdEndStrShort.length();
                String cmdLine = templateText.substring(p0 + cmdStartStrShort.length(), p - cmdEndStrShort.length());
                if (!processShortFormTemplateCommand(cmdLine, p0, p)) {
                    // If a short form command is not recognized, we process the whole command structure are normal
                    // text.
                    conditionalExclude(p0, p);
                }
            } else { // normal (long) form command
                p = templateText.indexOf(cmdEndStr, p0 + cmdStartStr.length());
                if (p == -1) {
                    throw new MiniTemplator.TemplateSyntaxException(
                            "Invalid HTML comment in template at offset " + p0 + ".");
                }
                p += cmdEndStr.length();
                String cmdLine = templateText.substring(p0 + cmdStartStr.length(), p - cmdEndStr.length());
                resumeCmdParsingFromStart = false;
                if (!processTemplateCommand(cmdLine, p0, p)) {
                    conditionalExclude(p0, p);
                } // process as normal temlate text
                if (resumeCmdParsingFromStart) { // (if a subtemplate has been included)
                    p = p0;
                }
            }
        }
    }

    // Returns false if the command should be treatet as normal template text.
    private boolean processTemplateCommand(String cmdLine, int cmdTPosBegin, int cmdTPosEnd)
            throws MiniTemplator.TemplateSyntaxException {
        int p0 = skipBlanks(cmdLine, 0);
        if (p0 >= cmdLine.length()) {
            return false;
        }
        int p = skipNonBlanks(cmdLine, p0);
        String cmd = cmdLine.substring(p0, p);
        String parms = cmdLine.substring(p);
        /* select */
        if (cmd.equalsIgnoreCase("$beginBlock")) {
            processBeginBlockCmd(parms, cmdTPosBegin, cmdTPosEnd);
        } else if (cmd.equalsIgnoreCase("$endBlock")) {
            processEndBlockCmd(parms, cmdTPosBegin, cmdTPosEnd);
        } else if (cmd.equalsIgnoreCase("$include")) {
            processIncludeCmd(parms, cmdTPosBegin, cmdTPosEnd);
        } else if (cmd.equalsIgnoreCase("$if")) {
            processIfCmd(parms, cmdTPosBegin, cmdTPosEnd);
        } else if (cmd.equalsIgnoreCase("$elseIf")) {
            processElseIfCmd(parms, cmdTPosBegin, cmdTPosEnd);
        } else if (cmd.equalsIgnoreCase("$else")) {
            processElseCmd(parms, cmdTPosBegin, cmdTPosEnd);
        } else if (cmd.equalsIgnoreCase("$endIf")) {
            processEndIfCmd(parms, cmdTPosBegin, cmdTPosEnd);
        } else {
            if (cmd.startsWith("$") && !cmd.startsWith("${")) {
                throw new MiniTemplator.TemplateSyntaxException(
                        "Unknown command \"" + cmd + "\" in template at offset " + cmdTPosBegin + ".");
            } else {
                return false;
            }
        }
        return true;
    }

    // Returns false if the command is not recognized and should be treatet as normal temlate text.
    private boolean processShortFormTemplateCommand(String cmdLine, int cmdTPosBegin, int cmdTPosEnd)
            throws MiniTemplator.TemplateSyntaxException {
        int p0 = skipBlanks(cmdLine, 0);
        if (p0 >= cmdLine.length()) {
            return false;
        }
        int p = p0;
        char cmd1 = cmdLine.charAt(p++);
        if (cmd1 == '/' && p < cmdLine.length() && !Character.isWhitespace(cmdLine.charAt(p))) {
            p++;
        }
        String cmd = cmdLine.substring(p0, p);
        String parms = cmdLine.substring(p).trim();
        /* select */
        if (cmd.equals("?")) {
            processIfCmd(parms, cmdTPosBegin, cmdTPosEnd);
        } else if (cmd.equals(":")) {
            if (parms.length() > 0) {
                processElseIfCmd(parms, cmdTPosBegin, cmdTPosEnd);
            } else {
                processElseCmd(parms, cmdTPosBegin, cmdTPosEnd);
            }
        } else if (cmd.equals("/?")) {
            processEndIfCmd(parms, cmdTPosBegin, cmdTPosEnd);
        } else {
            return false;
        }
        return true;
    }

    // Processes the $beginBlock command.
    private void processBeginBlockCmd(String parms, int cmdTPosBegin, int cmdTPosEnd)
            throws MiniTemplator.TemplateSyntaxException {
        if (conditionalExclude(cmdTPosBegin, cmdTPosEnd)) {
            return;
        }
        int p0 = skipBlanks(parms, 0);
        if (p0 >= parms.length()) {
            throw new MiniTemplator.TemplateSyntaxException(
                    "Missing block name in $BeginBlock command in template at offset " + cmdTPosBegin + ".");
        }
        int p = skipNonBlanks(parms, p0);
        String blockName = parms.substring(p0, p);
        if (!isRestOfStringBlank(parms, p)) {
            throw new MiniTemplator.TemplateSyntaxException(
                    "Extra parameter in $BeginBlock command in template at offset " + cmdTPosBegin + ".");
        }
        int blockNo = registerBlock(blockName);
        BlockTabRec btr = blockTab[blockNo];
        btr.tPosBegin = cmdTPosBegin;
        btr.tPosContentsBegin = cmdTPosEnd;
        openBlocksTab[currentNestingLevel] = blockNo;
        currentNestingLevel++;
        if (currentNestingLevel > maxNestingLevel) {
            throw new MiniTemplator.TemplateSyntaxException("Block nesting overflow for block \"" + blockName
                    + "\" in template at offset " + cmdTPosBegin + ".");
        }
    }

    // Processes the $endBlock command.
    private void processEndBlockCmd(String parms, int cmdTPosBegin, int cmdTPosEnd)
            throws MiniTemplator.TemplateSyntaxException {
        if (conditionalExclude(cmdTPosBegin, cmdTPosEnd)) {
            return;
        }
        int p0 = skipBlanks(parms, 0);
        if (p0 >= parms.length()) {
            throw new MiniTemplator.TemplateSyntaxException(
                    "Missing block name in $EndBlock command in template at offset " + cmdTPosBegin + ".");
        }
        int p = skipNonBlanks(parms, p0);
        String blockName = parms.substring(p0, p);
        if (!isRestOfStringBlank(parms, p)) {
            throw new MiniTemplator.TemplateSyntaxException(
                    "Extra parameter in $EndBlock command in template at offset " + cmdTPosBegin + ".");
        }
        int blockNo = lookupBlockName(blockName);
        if (blockNo == -1) {
            throw new MiniTemplator.TemplateSyntaxException("Undefined block name \"" + blockName
                    + "\" in $EndBlock command in template at offset " + cmdTPosBegin + ".");
        }
        currentNestingLevel--;
        BlockTabRec btr = blockTab[blockNo];
        if (!btr.definitionIsOpen) {
            throw new MiniTemplator.TemplateSyntaxException("Multiple $EndBlock command for block \"" + blockName
                    + "\" in template at offset " + cmdTPosBegin + ".");
        }
        if (btr.nestingLevel != currentNestingLevel) {
            throw new MiniTemplator.TemplateSyntaxException(
                    "Block nesting level mismatch at $EndBlock command for block \"" + blockName
                            + "\" in template at offset " + cmdTPosBegin + ".");
        }
        btr.tPosContentsEnd = cmdTPosBegin;
        btr.tPosEnd = cmdTPosEnd;
        btr.definitionIsOpen = false;
    }

    // Returns the block number of the newly registered block.
    private int registerBlock(String blockName) {
        int blockNo = blockTabCnt++;
        if (blockTabCnt > blockTab.length) {
            blockTab = (BlockTabRec[]) resizeArray(blockTab, 2 * blockTabCnt);
        }
        BlockTabRec btr = new BlockTabRec();
        blockTab[blockNo] = btr;
        btr.blockName = blockName;
        if (blockName != null) {
            btr.nextWithSameName = lookupBlockName(blockName);
        } else {
            btr.nextWithSameName = -1;
        }
        btr.nestingLevel = currentNestingLevel;
        if (currentNestingLevel > 0) {
            btr.parentBlockNo = openBlocksTab[currentNestingLevel - 1];
        } else {
            btr.parentBlockNo = -1;
        }
        btr.definitionIsOpen = true;
        btr.blockVarCnt = 0;
        btr.firstVarRefNo = -1;
        btr.blockVarNoToVarNoMap = new int[32];
        btr.dummy = false;
        if (blockName != null) {
            blockNameToNoMap.put(blockName.toUpperCase(), new Integer(blockNo));
        }
        return blockNo;
    }

    // Registers a dummy block to exclude a range within the template text.
    private void excludeTemplateRange(int tPosBegin, int tPosEnd) {
        if (blockTabCnt > 0) {
            // Check whether we can extend the previous block.
            BlockTabRec btr = blockTab[blockTabCnt - 1];
            if (btr.dummy && btr.tPosEnd == tPosBegin) {
                btr.tPosContentsEnd = tPosEnd;
                btr.tPosEnd = tPosEnd;
                return;
            }
        }
        int blockNo = registerBlock(null);
        BlockTabRec btr = blockTab[blockNo];
        btr.tPosBegin = tPosBegin;
        btr.tPosContentsBegin = tPosBegin;
        btr.tPosContentsEnd = tPosEnd;
        btr.tPosEnd = tPosEnd;
        btr.definitionIsOpen = false;
        btr.dummy = true;
    }

    // Checks that all block definitions are closed.
    private void checkBlockDefinitionsComplete() throws MiniTemplator.TemplateSyntaxException {
        for (int blockNo = 0; blockNo < blockTabCnt; blockNo++) {
            BlockTabRec btr = blockTab[blockNo];
            if (btr.definitionIsOpen) {
                throw new MiniTemplator.TemplateSyntaxException(
                        "Missing $EndBlock command in template for block \"" + btr.blockName + "\".");
            }
        }
        if (currentNestingLevel != 0) {
            throw new MiniTemplator.TemplateSyntaxException("Block nesting level error at end of template.");
        }
    }

    // Processes the $include command.
    private void processIncludeCmd(String parms, int cmdTPosBegin, int cmdTPosEnd)
            throws MiniTemplator.TemplateSyntaxException {
        if (conditionalExclude(cmdTPosBegin, cmdTPosEnd)) {
            return;
        }
        int p0 = skipBlanks(parms, 0);
        if (p0 >= parms.length()) {
            throw new MiniTemplator.TemplateSyntaxException(
                    "Missing subtemplate name in $Include command in template at offset " + cmdTPosBegin + ".");
        }
        int p;
        if (parms.charAt(p0) == '"') { // subtemplate name is quoted
            p0++;
            p = parms.indexOf('"', p0);
            if (p == -1) {
                throw new MiniTemplator.TemplateSyntaxException(
                        "Missing closing quote for subtemplate name in $Include command in template at offset "
                                + cmdTPosBegin + ".");
            }
        } else {
            p = skipNonBlanks(parms, p0);
        }
        String subtemplateName = parms.substring(p0, p);
        p++;
        if (!isRestOfStringBlank(parms, p)) {
            throw new MiniTemplator.TemplateSyntaxException(
                    "Extra parameter in $Include command in template at offset " + cmdTPosBegin + ".");
        }
        insertSubtemplate(subtemplateName, cmdTPosBegin, cmdTPosEnd);
    }

    private void insertSubtemplate(String subtemplateName, int tPos1, int tPos2) {
        if (templateText.length() > maxInclTemplateSize) {
            throw new RuntimeException(
                    "Subtemplate include aborted because the internal template string is longer than "
                            + maxInclTemplateSize + " characters.");
        }
        String subtemplate;
        try {
            subtemplate = miniTemplator.loadSubtemplate(subtemplateName);
        } catch (IOException e) {
            throw new RuntimeException("Error while loading subtemplate \"" + subtemplateName + "\"", e);
        }
        // (Copying the template to insert a subtemplate is a bit slow. In a future implementation of MiniTemplator,
        // a table could be used that contains references to the string fragments.)
        StringBuilder s = new StringBuilder(templateText.length() + subtemplate.length());
        s.append(templateText, 0, tPos1);
        s.append(subtemplate);
        s.append(templateText, tPos2, templateText.length());
        templateText = s.toString();
        resumeCmdParsingFromStart = true;
    }

    // --- Conditional commands -----------------------------------------------------

    // Returns the enabled/disabled state of the condition at level condLevel2.
    private boolean isCondEnabled(int condLevel2) {
        if (condLevel2 < 0) {
            return true;
        }
        return condEnabled[condLevel2];
    }

    // If the current condition is disabled, the text from tPosBegin to tPosEnd
    // is excluded and true is returned.
    // Otherwise nothing is done and false is returned.
    private boolean conditionalExclude(int tPosBegin, int tPosEnd) {
        if (isCondEnabled(condLevel)) {
            return false;
        }
        excludeTemplateRange(tPosBegin, tPosEnd);
        return true;
    }

    // Evaluates a condition expression of a conditional command, by comparing the
    // flags in the expression with the flags in TemplateSpecification.conditionFlags.
    // Returns true the condition is met.
    private boolean evaluateConditionFlags(String flags) {
        int p = 0;
        while (true) {
            p = skipBlanks(flags, p);
            if (p >= flags.length()) {
                break;
            }
            boolean complement = false;
            if (flags.charAt(p) == '!') {
                complement = true;
                p++;
            }
            p = skipBlanks(flags, p);
            if (p >= flags.length()) {
                break;
            }
            int p0 = p;
            p = skipNonBlanks(flags, p0 + 1);
            String flag = flags.substring(p0, p).toUpperCase();
            if ((conditionFlags != null && conditionFlags.contains(flag)) ^ complement) {
                return true;
            }
        }
        return false;
    }

    // Processes the $if command.
    private void processIfCmd(String parms, int cmdTPosBegin, int cmdTPosEnd)
            throws MiniTemplator.TemplateSyntaxException {
        excludeTemplateRange(cmdTPosBegin, cmdTPosEnd);
        if (condLevel >= maxCondLevels - 1) {
            throw new MiniTemplator.TemplateSyntaxException("Too many nested $if commands.");
        }
        condLevel++;
        boolean enabled = isCondEnabled(condLevel - 1) && evaluateConditionFlags(parms);
        condEnabled[condLevel] = enabled;
        condPassed[condLevel] = enabled;
    }

    // Processes the $elseIf command.
    private void processElseIfCmd(String parms, int cmdTPosBegin, int cmdTPosEnd)
            throws MiniTemplator.TemplateSyntaxException {
        excludeTemplateRange(cmdTPosBegin, cmdTPosEnd);
        if (condLevel < 0) {
            throw new MiniTemplator.TemplateSyntaxException("$elseIf without matching $if.");
        }
        boolean enabled = isCondEnabled(condLevel - 1) && !condPassed[condLevel] && evaluateConditionFlags(parms);
        condEnabled[condLevel] = enabled;
        if (enabled) {
            condPassed[condLevel] = true;
        }
    }

    // Processes the $else command.
    private void processElseCmd(String parms, int cmdTPosBegin, int cmdTPosEnd)
            throws MiniTemplator.TemplateSyntaxException {
        excludeTemplateRange(cmdTPosBegin, cmdTPosEnd);
        if (parms.trim().length() != 0) {
            throw new MiniTemplator.TemplateSyntaxException("Invalid parameters for $else command.");
        }
        if (condLevel < 0) {
            throw new MiniTemplator.TemplateSyntaxException("$else without matching $if.");
        }
        boolean enabled = isCondEnabled(condLevel - 1) && !condPassed[condLevel];
        condEnabled[condLevel] = enabled;
        if (enabled) {
            condPassed[condLevel] = true;
        }
    }

    // Processes the $endIf command.
    private void processEndIfCmd(String parms, int cmdTPosBegin, int cmdTPosEnd)
            throws MiniTemplator.TemplateSyntaxException {
        excludeTemplateRange(cmdTPosBegin, cmdTPosEnd);
        if (parms.trim().length() != 0) {
            throw new MiniTemplator.TemplateSyntaxException("Invalid parameters for $endIf command.");
        }
        if (condLevel < 0) {
            throw new MiniTemplator.TemplateSyntaxException("$endif without matching $if.");
        }
        condLevel--;
    }

    // ------------------------------------------------------------------------------

    // Associates variable references with blocks.
    private void associateVariablesWithBlocks() {
        int varRefNo = 0;
        int activeBlockNo = 0;
        int nextBlockNo = 1;
        while (varRefNo < varRefTabCnt) {
            VarRefTabRec vrtr = varRefTab[varRefNo];
            int varRefTPos = vrtr.tPosBegin;
            int varNo = vrtr.varNo;
            if (varRefTPos >= blockTab[activeBlockNo].tPosEnd) {
                activeBlockNo = blockTab[activeBlockNo].parentBlockNo;
                continue;
            }
            if (nextBlockNo < blockTabCnt && varRefTPos >= blockTab[nextBlockNo].tPosBegin) {
                activeBlockNo = nextBlockNo;
                nextBlockNo++;
                continue;
            }
            BlockTabRec btr = blockTab[activeBlockNo];
            if (varRefTPos < btr.tPosBegin) {
                throw new AssertionError();
            }
            int blockVarNo = btr.blockVarCnt++;
            if (btr.blockVarCnt > btr.blockVarNoToVarNoMap.length) {
                btr.blockVarNoToVarNoMap = (int[]) resizeArray(btr.blockVarNoToVarNoMap, 2 * btr.blockVarCnt);
            }
            btr.blockVarNoToVarNoMap[blockVarNo] = varNo;
            if (btr.firstVarRefNo == -1) {
                btr.firstVarRefNo = varRefNo;
            }
            vrtr.blockNo = activeBlockNo;
            vrtr.blockVarNo = blockVarNo;
            varRefNo++;
        }
    }

    // Parses variable references within the template in the format "${VarName}" .
    private void parseTemplateVariables() throws MiniTemplator.TemplateSyntaxException {
        int p = 0;
        while (true) {
            p = templateText.indexOf("${", p);
            if (p == -1) {
                break;
            }
            int p0 = p;
            p = templateText.indexOf("}", p);
            if (p == -1) {
                throw new MiniTemplator.TemplateSyntaxException(
                        "Invalid variable reference in template at offset " + p0 + ".");
            }
            p++;
            String varName = templateText.substring(p0 + 2, p - 1).trim();
            if (varName.length() == 0) {
                throw new MiniTemplator.TemplateSyntaxException(
                        "Empty variable name in template at offset " + p0 + ".");
            }
            registerVariableReference(varName, p0, p);
        }
    }

    private void registerVariableReference(String varName, int tPosBegin, int tPosEnd) {
        int varNo;
        varNo = lookupVariableName(varName);
        if (varNo == -1) {
            varNo = registerVariable(varName);
        }
        int varRefNo = varRefTabCnt++;
        if (varRefTabCnt > varRefTab.length) {
            varRefTab = (VarRefTabRec[]) resizeArray(varRefTab, 2 * varRefTabCnt);
        }
        VarRefTabRec vrtr = new VarRefTabRec();
        varRefTab[varRefNo] = vrtr;
        vrtr.tPosBegin = tPosBegin;
        vrtr.tPosEnd = tPosEnd;
        vrtr.varNo = varNo;
    }

    // Returns the variable number of the newly registered variable.
    private int registerVariable(String varName) {
        int varNo = varTabCnt++;
        if (varTabCnt > varTab.length) {
            varTab = (String[]) resizeArray(varTab, 2 * varTabCnt);
        }
        varTab[varNo] = varName;
        varNameToNoMap.put(varName.toUpperCase(), new Integer(varNo));
        return varNo;
    }

    // --- name lookup routines -------------------------------------------

    // Maps variable name to variable number.
    // Returns -1 if the variable name is not found.
    public int lookupVariableName(String varName) {
        Integer varNoWrapper = varNameToNoMap.get(varName.toUpperCase());
        if (varNoWrapper == null) {
            return -1;
        }
        int varNo = varNoWrapper.intValue();
        return varNo;
    }

    // Maps block name to block number.
    // If there are multiple blocks with the same name, the block number of the last
    // registered block with that name is returned.
    // Returns -1 if the block name is not found.
    public int lookupBlockName(String blockName) {
        Integer blockNoWrapper = blockNameToNoMap.get(blockName.toUpperCase());
        if (blockNoWrapper == null) {
            return -1;
        }
        int blockNo = blockNoWrapper.intValue();
        return blockNo;
    }

    // --- general utility routines ---------------------------------------

    // Reallocates an array with a new size and copies the contents
    // of the old array to the new array.
    public static Object resizeArray(Object oldArray, int newSize) {
        int oldSize = java.lang.reflect.Array.getLength(oldArray);
        Class<?> elementType = oldArray.getClass().getComponentType();
        Object newArray = java.lang.reflect.Array.newInstance(elementType, newSize);
        int preserveLength = Math.min(oldSize, newSize);
        if (preserveLength > 0) {
            System.arraycopy(oldArray, 0, newArray, 0, preserveLength);
        }
        return newArray;
    }

    // Skips blanks (white space) in string s starting at position p.
    private static int skipBlanks(String s, int p) {
        while (p < s.length() && Character.isWhitespace(s.charAt(p)))
            p++;
        return p;
    }

    // Skips non-blanks (no-white space) in string s starting at position p.
    private static int skipNonBlanks(String s, int p) {
        while (p < s.length() && !Character.isWhitespace(s.charAt(p)))
            p++;
        return p;
    }

    // Returns true if string s is blank (white space) from position p to the end.
    public static boolean isRestOfStringBlank(String s, int p) {
        return skipBlanks(s, p) >= s.length();
    }

} // End class MiniTemplatorParser